diff options
| author | Factiven <[email protected]> | 2023-12-24 13:03:54 +0700 |
|---|---|---|
| committer | Factiven <[email protected]> | 2023-12-24 13:03:54 +0700 |
| commit | 50a0f0240d7fef133eb5acc1bea2b1168b08e9db (patch) | |
| tree | 307e09e505580415a58d64b5fc3580e9235869f1 /pages/en/anime/[...id].tsx | |
| parent | Update README.md (#104) (diff) | |
| download | moopa-50a0f0240d7fef133eb5acc1bea2b1168b08e9db.tar.xz moopa-50a0f0240d7fef133eb5acc1bea2b1168b08e9db.zip | |
migrate to typescript
Diffstat (limited to 'pages/en/anime/[...id].tsx')
| -rw-r--r-- | pages/en/anime/[...id].tsx | 336 |
1 files changed, 336 insertions, 0 deletions
diff --git a/pages/en/anime/[...id].tsx b/pages/en/anime/[...id].tsx new file mode 100644 index 0000000..42cae38 --- /dev/null +++ b/pages/en/anime/[...id].tsx @@ -0,0 +1,336 @@ +import Head from "next/head"; +import Image from "next/image"; +import { useRouter } from "next/router"; +import { useEffect, useState } from "react"; +import Content from "@/components/home/content"; +import Modal from "@/components/modal"; + +import { signIn, useSession } from "next-auth/react"; +import AniList from "@/components/media/aniList"; +import ListEditor from "@/components/listEditor"; + +import DetailTop from "@/components/anime/mobile/topSection"; +import AnimeEpisode from "@/components/anime/episode"; +import { useAniList } from "@/lib/anilist/useAnilist"; +import Footer from "@/components/shared/footer"; +import { mediaInfoQuery } from "@/lib/graphql/query"; +import MobileNav from "@/components/shared/MobileNav"; + +import pls from "@/utils/request/index"; + +import Characters from "@/components/anime/charactersCard"; +import { redis } from "@/lib/redis"; +import { toast } from "sonner"; +import { Navbar } from "@/components/shared/NavBar"; +import { AniListInfoTypes } from "types/info/AnilistInfoTypes"; + +type InfoTypes = { + info: AniListInfoTypes; + color: string; + api: string; + chapterNotFound: string; +}; + +export default function Info({ info, color, chapterNotFound }: InfoTypes) { + const { data: session }: any = useSession(); + const { getUserLists } = useAniList(session); + + const [loading, setLoading] = useState(false); + const [progress, setProgress] = useState<number>(0); + const [statuses, setStatuses] = useState<any>(null); + const [domainUrl, setDomainUrl] = useState(""); + const [watch, setWatch] = useState<string>(); + + const [open, setOpen] = useState(false); + const { id } = useRouter().query; + + const rec = info?.recommendations?.nodes?.map( + (data) => data.mediaRecommendation + ); + + useEffect(() => { + if (chapterNotFound) { + toast.error("Source not found"); + const cleanUrl = window.location.origin + window.location.pathname; + window.history.replaceState(null, "", cleanUrl); + } + }, [chapterNotFound]); + + useEffect(() => { + handleClose(); + async function fetchData() { + setLoading(true); + if (id) { + try { + setDomainUrl(window.location.origin); + + setProgress(0); + setStatuses(null); + + if (session?.user?.name) { + const res = await getUserLists(info.id); + const user = res?.data?.Media?.mediaListEntry; + + if (user) { + setProgress(user.progress); + const statusMapping: { + [key: string]: { name: string; value: string }; + } = { + CURRENT: { name: "Watching", value: "CURRENT" }, + PLANNING: { name: "Plan to watch", value: "PLANNING" }, + COMPLETED: { name: "Completed", value: "COMPLETED" }, + DROPPED: { name: "Dropped", value: "DROPPED" }, + PAUSED: { name: "Paused", value: "PAUSED" }, + REPEATING: { name: "Rewatching", value: "REPEATING" }, + }; + setStatuses(statusMapping[user.status]); + } + } + } catch (error) { + console.error(error); + } finally { + setLoading(false); + } + } + } + fetchData(); + + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [id, info, session?.user?.name]); + + function handleOpen() { + setOpen(true); + document.body.style.overflow = "hidden"; + } + + function handleClose() { + setOpen(false); + document.body.style.overflow = "auto"; + } + + return ( + <> + <Head> + <title> + {info + ? info?.title?.romaji || info?.title?.english + : "Retrieving Data..."} + </title> + <meta + name="title" + content={info?.title?.romaji} + data-title-romaji={info?.title?.romaji} + data-title-english={info?.title?.english} + data-title-native={info?.title?.native} + /> + <meta name="description" content={info.description} /> + <meta name="twitter:card" content="summary_large_image" /> + <meta + name="twitter:title" + content={`Moopa - ${info.title.romaji || info.title.english}`} + /> + <meta + name="twitter:description" + content={`${info.description?.slice(0, 180)}...`} + /> + <meta + name="twitter:image" + content={`${domainUrl}/api/og?title=${ + info.title.romaji || info.title.english + }&image=${info.bannerImage || info.coverImage.extraLarge}`} + /> + </Head> + <Navbar info={info} /> + <Modal open={open} onClose={() => handleClose()}> + <div> + {!session && ( + <div className="flex-center flex-col gap-5 px-10 py-5 bg-secondary rounded-md"> + <div className="text-md font-extrabold font-karla"> + Edit your list + </div> + <button + className="flex items-center bg-[#363642] rounded-md text-white p-1" + onClick={() => signIn("AniListProvider")} + > + <h1 className="px-1 font-bold font-karla"> + Login with AniList + </h1> + <div className="scale-[60%] pb-[1px]"> + <AniList /> + </div> + </button> + </div> + )} + {session && info && ( + <ListEditor + animeId={info?.id} + session={session} + stats={statuses?.value} + prg={progress} + max={info?.episodes} + info={info} + close={handleClose} + /> + )} + </div> + </Modal> + <MobileNav hideProfile={true} /> + <main className="w-screen min-h-screen relative flex flex-col items-center bg-primary gap-5"> + <div className="w-screen absolute"> + <div className="bg-gradient-to-t from-primary from-10% to-transparent absolute h-[280px] w-screen z-10 inset-0" /> + {info?.bannerImage && ( + <Image + src={info?.bannerImage} + alt="banner anime" + height={1000} + width={1000} + blurDataURL={info?.bannerImage} + className="object-cover bg-image blur-[2px] w-screen absolute top-0 left-0 h-[250px] brightness-[55%] z-0" + /> + )} + </div> + <div className="w-full lg:max-w-screen-lg xl:max-w-screen-2xl z-30 flex flex-col gap-5"> + <DetailTop + info={info} + handleOpen={handleOpen} + statuses={statuses} + watchUrl={watch} + progress={progress || 0} + color={color} + /> + + <AnimeEpisode + info={info} + session={session} + progress={progress} + setProgress={setProgress} + setWatch={setWatch} + /> + + {info?.characters?.edges && ( + <div className="w-full"> + {/* <div className="w-full h-[150px] bg-white flex-center text-black"> + ad banner + </div> */} + <Characters info={info?.characters?.edges} /> + </div> + )} + + {info && rec?.length !== 0 && ( + <div className="w-full"> + <Content + ids="recommendAnime" + section="Recommendations" + data={rec} + /> + </div> + )} + </div> + </main> + <Footer /> + </> + ); +} + +export async function getServerSideProps(ctx: any) { + const { id, notfound } = ctx.query; + + let API_URI; + API_URI = process.env.API_URI || null || null; + if (API_URI && API_URI.endsWith("/")) { + API_URI = API_URI.slice(0, -1); + } + + let cache, chapterNotFound; + + if (notfound) { + // create random id string + chapterNotFound = Math.random().toString(36).substring(7); + } + + if (redis) { + cache = await redis.get(`anime:${id}`); + } + + if (cache) { + const { info, color } = JSON.parse(cache); + return { + props: { + info, + color, + api: API_URI, + chapterNotFound: chapterNotFound || null, + }, + }; + } else { + const [resp] = await pls.post("https://graphql.anilist.co/", { + // method: "POST", + // headers: { + // "Content-Type": "application/json", + // }, + body: JSON.stringify({ + query: mediaInfoQuery, + variables: { + id: id?.[0], + }, + }), + }); + + // const json = await resp.json(); + const data = resp?.data?.Media; + + const cacheTime = data?.nextAiringEpisode?.episode + ? 60 * 10 + : 60 * 60 * 24 * 30; + + if (!data) { + return { + notFound: true, + }; + } + + const textColor = setTxtColor(data?.coverImage?.color); + + const color = { + backgroundColor: `${data?.coverImage?.color || "#ffff"}`, + color: textColor, + }; + + if (redis) { + await redis.set( + `anime:${id}`, + JSON.stringify({ + info: data, + color: color, + }), + "EX", + cacheTime + ); + } + + return { + props: { + info: data, + color: color, + api: API_URI, + chapterNotFound: chapterNotFound || null, + }, + }; + } +} + +function getBrightness(hexColor: { match: (arg0: RegExp) => any[] }) { + if (!hexColor) { + return 200; + } + const rgb = hexColor + .match(/^#?([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})$/i) + .slice(1) + .map((x) => parseInt(x, 16)); + return (299 * rgb[0] + 587 * rgb[1] + 114 * rgb[2]) / 1000; +} + +function setTxtColor(hexColor: { match: (arg0: RegExp) => any[] }) { + const brightness = getBrightness(hexColor); + return brightness < 150 ? "#fff" : "#000"; +} |